Fix pow(), ** and fpow() losing precision on 32-bit x86 - #23578
Open
marc-mabe wants to merge 2 commits into
Open
Fix pow(), ** and fpow() losing precision on 32-bit x86#23578marc-mabe wants to merge 2 commits into
marc-mabe wants to merge 2 commits into
Conversation
On i386 the x87 FPU is the platform default and zend_init_fpu() clamps it to double precision (53-bit mantissa) for the whole request. libm's pow() computes on the x87 unit and relies on the extended range to round correctly to double, so while that clamp is in place exponentiation is off by one or more ULP. This matters because it makes exponentiation silently architecture- dependent: the same script prints different floats on i386 than on x86-64 or arm64, comparisons against precomputed constants fail, and the error propagates into anything built on it. Three code paths reach libm pow() and each gets its own file: Zend/tests/pow_fpu_precision.phpt the ** operator ext/standard/tests/math/pow_fpu_precision.phpt the pow() function ext/standard/tests/math/fpow_fpu_precision.phpt the fpow() function The operator and the function are covered by the same cases with the same inputs, so the two files differ only in call syntax and share an identical --EXPECT-- block. fpow() is kept separate because it bypasses safe_pow() and calls libm pow() directly, and because it additionally asserts the IEEE-754 special cases that must survive the FPU precision switch. Every expected value is exactly the correctly rounded double of its decimal literal, so a build that keeps full precision prints the short literal back and compares identical to it. Exact powers of two are included as reference points the clamp does not affect. These tests fail on i386 and pass wherever XPFPA_HAVE_CW is 0, which includes x86-64 and arm64. The following commit fixes i386.
Restore extended FPU precision around the libm pow() call and truncate the result back to double, so that i386 returns the same correctly rounded doubles as every other platform. zend_pow() in zend_float.h wraps the call and is used from both safe_pow(), which backs the ** operator and pow(), and fpow(), which called libm pow() directly. It compiles down to a plain pow() wherever XPFPA_HAVE_CW is 0, which includes x86-64 and arm64.
This was referenced Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On 32-bit x86, the
**operator,pow()andfpow()return results that are off by one or more ULP compared to every other platform:The results are not merely different, they are less accurate: measured against a high-precision reference, the i386 values are not correctly rounded while the x86-64 and arm64 ones are. This makes exponentiation silently architecture-dependent, so float comparisons against literals fail, cached or serialized values differ between machines, and the error propagates into anything built on top.
Cause
init_executor()callszend_init_fpu(), which clamps the x87 FPU to double precision (a 53-bit mantissa) for the whole request so that PHP's owndoublearithmetic behaves like IEEE-754 binary64 rather than carrying x87's 64-bit excess precision.libm's
pow(), however, computes on that same x87 unit and relies on the extended range internally in order to round correctly todouble. While the clamp is in place it loses that headroom, sopow()inside PHP is less accurate than the very same libmpow()is outside PHP.Fix
zend_pow()inZend/zend_float.hrestores extended precision for the duration of the libm call and truncates the result back todoubleon return, using the XPFPA macros already in that header. Both call sites go through it:safe_pow()inZend/zend_operators.c, which backs the**operator andpow()PHP_FUNCTION(fpow)inext/standard/math.c, which called libmpow()directly and so did not benefit from a fix tosafe_pow()aloneWherever
XPFPA_HAVE_CWis 0 — x86-64, arm64, and every other target without x87 precision control —zend_pow()compiles down to a plainpow()call, so no other platform is affected in behaviour or code generation.The i386 failure is exactly the precision assertions; the reference points and the IEEE-754 cases pass in both states.
Notes
--enable-zend-int64#19079 where some tests were failing because of these differencesexp,expm1andsinhwill follow